/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s:
 * Copyright © 2018 Red Hat, Inc
 * Copyright © 2024 GNOME Foundation, Inc.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors:
 *       Alexander Larsson <alexl@redhat.com>
 *       Hubert Figuière <hub@figuiere.net>
 */

#include "config.h"

#include "flatpak-cli-transaction.h"
#include "flatpak-transaction-private.h"
#include "flatpak-installation-private.h"
#include "flatpak-run-private.h"
#include "flatpak-table-printer.h"
#include "flatpak-tty-utils-private.h"
#include "flatpak-utils-private.h"
#include "flatpak-error.h"
#include <glib/gi18n.h>


struct _FlatpakCliTransaction
{
  FlatpakTransaction   parent;

  gboolean             disable_interaction;
  gboolean             stop_on_first_error;
  gboolean             non_default_arch;
  GError              *first_operation_error;

  GHashTable          *eol_actions;
  GHashTable          *runtime_app_map;
  GHashTable          *extension_app_map;

  int                  rows;
  int                  cols;
  int                  table_width;
  int                  table_height;

  int                  n_ops;
  int                  op;
  int                  op_progress;

  gboolean             installing;
  gboolean             updating;
  gboolean             uninstalling;

  int                  download_col;

  FlatpakTablePrinter *printer;
  int                  progress_row;
  char                *progress_msg;
  int                  speed_len;

  gboolean             did_interaction;
};

struct _FlatpakCliTransactionClass
{
  FlatpakTransactionClass parent_class;
};

G_DEFINE_TYPE (FlatpakCliTransaction, flatpak_cli_transaction, FLATPAK_TYPE_TRANSACTION);

static int
choose_remote_for_ref (FlatpakTransaction *transaction,
                       const char         *for_ref,
                       const char         *runtime_ref,
                       const char * const *remotes)
{
  FlatpakCliTransaction *self = FLATPAK_CLI_TRANSACTION (transaction);
  int n_remotes = g_strv_length ((char **) remotes);
  int chosen = -1;
  const char *pref;

  pref = strchr (for_ref, '/') + 1;

  self->did_interaction = TRUE;

  if (self->disable_interaction)
    {
      g_print (_("Required runtime for %s (%s) found in remote %s\n"),
               pref, runtime_ref, remotes[0]);
      chosen = 0;
    }
  else if (n_remotes == 1)
    {
      g_print (_("Required runtime for %s (%s) found in remote %s\n"),
               pref, runtime_ref, remotes[0]);
      if (flatpak_yes_no_prompt (TRUE, _("Do you want to install it?")))
        chosen = 0;
    }
  else
    {
      flatpak_format_choices ((const char **) remotes,
                              _("Required runtime for %s (%s) found in remotes:"),
                              pref, runtime_ref);
      chosen = flatpak_number_prompt (TRUE, 0, n_remotes, _("Which do you want to install (0 to abort)?"));
      chosen -= 1; /* convert from base-1 to base-0 (and -1 to abort) */
    }

  return chosen;
}

static gboolean
add_new_remote (FlatpakTransaction            *transaction,
                FlatpakTransactionRemoteReason reason,
                const char                    *from_id,
                const char                    *remote_name,
                const char                    *url)
{
  FlatpakCliTransaction *self = FLATPAK_CLI_TRANSACTION (transaction);

  self->did_interaction = TRUE;

  if (self->disable_interaction)
    {
      g_print (_("Configuring %s as new remote '%s'\n"), url, remote_name);
      return TRUE;
    }

  if (reason == FLATPAK_TRANSACTION_REMOTE_GENERIC_REPO)
    {
      if (flatpak_yes_no_prompt (TRUE, /* default to yes on Enter */
                                 _("The remote '%s', referred to by '%s' at location %s contains additional applications.\n"
                                   "Should the remote be kept for future installations?"),
                                 remote_name, from_id, url))
        return TRUE;
    }
  else if (reason == FLATPAK_TRANSACTION_REMOTE_RUNTIME_DEPS)
    {
      if (flatpak_yes_no_prompt (TRUE, /* default to yes on Enter */
                                 _("The application %s depends on runtimes from:\n  %s\n"
                                   "Configure this as new remote '%s'"),
                                 from_id, url, remote_name))
        return TRUE;
    }

  return FALSE;
}

static void
install_authenticator (FlatpakTransaction            *old_transaction,
                       const char                    *remote,
                       const char                    *ref)
{
  FlatpakCliTransaction *old_cli = FLATPAK_CLI_TRANSACTION (old_transaction);
  g_autoptr(FlatpakTransaction)  transaction2 = NULL;
  g_autoptr(GError) local_error = NULL;
  g_autoptr(FlatpakInstallation) installation = flatpak_transaction_get_installation (old_transaction);
  g_autoptr(FlatpakDir) dir = flatpak_installation_get_dir (installation, NULL);

  if (dir == NULL)
    {
      /* This should not happen */
      g_warning ("No dir in install_authenticator");
      return;
    }

  old_cli->did_interaction = TRUE;

  transaction2 = flatpak_cli_transaction_new (dir, old_cli->disable_interaction, TRUE, FALSE, &local_error);
  if (transaction2 == NULL)
    {
      g_printerr ("Unable to install authenticator: %s\n", local_error->message);
      return;
    }

  g_print ("Installing required authenticator for remote %s\n", remote);
  if (!flatpak_transaction_add_install (transaction2, remote, ref, NULL, &local_error))
    {
      if (!g_error_matches (local_error, FLATPAK_ERROR, FLATPAK_ERROR_ALREADY_INSTALLED))
        g_printerr ("Unable to install authenticator: %s\n", local_error->message);
      return;
    }

  if (!flatpak_transaction_run (transaction2, NULL, &local_error))
    {
      if (!g_error_matches (local_error, FLATPAK_ERROR, FLATPAK_ERROR_ABORTED))
        g_printerr ("Unable to install authenticator: %s\n", local_error->message);
      return;
    }

  return;
}

static gboolean
redraw (FlatpakCliTransaction *self)
{
  int top;
  int row;
  int current_row;
  int current_col;
  int skip;

  /* We may have resized and thus repositioned the cursor since last redraw */
  flatpak_get_window_size (&self->rows, &self->cols);
  if (flatpak_get_cursor_pos (&current_row, &current_col))
    {
      /* We're currently displaying the last row of the table, extept the
         very first time where the user pressed return for the prompt causing us
         to scroll down one extra row */
      top = current_row - self->table_height + 1;
      if (top > 0)
        {
          row = top;
          skip = 0;
        }
      else
        {
          row = 1;
          skip = 1 - top;
        }

      g_print (FLATPAK_ANSI_ROW_N FLATPAK_ANSI_CLEAR, row);
      // we update table_height and end_row here, since we might have added to the table
      flatpak_table_printer_print_full (self->printer, skip, self->cols,
                                        &self->table_height, &self->table_width);
      return TRUE;
    }
  return FALSE;
}

static void
set_op_progress (FlatpakCliTransaction       *self,
                 FlatpakTransactionOperation *op,
                 const char                  *progress)
{
  if (flatpak_fancy_output ())
    {
      int row = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (op), "row"));
      g_autofree char *cell = g_strdup_printf ("[%s]", progress);
      flatpak_table_printer_set_cell (self->printer, row, 1, cell);
    }
}

static void
spin_op_progress (FlatpakCliTransaction       *self,
                  FlatpakTransactionOperation *op)
{
  const char *p[] = {
    "|",
    "/",
    "—",
    "\\",
  };

  set_op_progress (self, op, p[self->op_progress++ % G_N_ELEMENTS (p)]);
}

static char *
format_duration (guint64 duration)
{
  int h, m, s;

  m = duration / 60;
  s = duration % 60;
  h = m / 60;
  m = m % 60;

  if (h > 0)
    return g_strdup_printf ("%02d:%02d:%02d", h, m, s);
  else
    return g_strdup_printf ("%02d:%02d", m, s);
}

static void
progress_changed_cb (FlatpakTransactionProgress *progress,
                     gpointer                    data)
{
  FlatpakCliTransaction *cli = data;
  FlatpakTransaction *self = FLATPAK_TRANSACTION (cli);
  g_autoptr(FlatpakTransactionOperation) op = flatpak_transaction_get_current_operation (self);
  g_autoptr(GString) str = g_string_new ("");
  int i;
  int n_full, partial;
  g_autofree char *speed = NULL;
  int bar_length;
  const char *partial_blocks[] = {
    " ",
    "▏",
    "▎",
    "▍",
    "▌",
    "▋",
    "▊",
    "▉",
  };
  const char *full_block = "█";

  guint percent = flatpak_transaction_progress_get_progress (progress);
  guint64 start_time = flatpak_transaction_progress_get_start_time (progress);
  guint64 elapsed_time = (g_get_monotonic_time () - start_time) / G_USEC_PER_SEC;
  guint64 transferred = flatpak_transaction_progress_get_bytes_transferred (progress);
  guint64 max = flatpak_transaction_operation_get_download_size (op);

  if (elapsed_time > 0)
    {
      g_autofree char *formatted_bytes_sec = g_format_size (transferred / elapsed_time);
      g_autofree char *remaining = NULL;
      if (elapsed_time > 3 && percent > 0)
        {
          guint64 total_time = elapsed_time * 100 / (double) percent;
          remaining = format_duration (total_time - elapsed_time);
        }
      /* Formatted size/remaining time in seconds */
      speed = g_strdup_printf (_("%s/s%s%s"), formatted_bytes_sec, remaining ? "  " : "", remaining ? remaining : "");
      cli->speed_len = MAX (cli->speed_len, strlen (speed) + 2);
    }

  spin_op_progress (cli, op);

  bar_length = MIN (20, cli->table_width - (strlen (cli->progress_msg) + 6 + cli->speed_len));

  n_full = (bar_length * percent) / 100;
  partial = (((bar_length * percent) % 100) * G_N_ELEMENTS (partial_blocks)) / 100;
  /* The above should guarantee this: */
  g_assert (partial >= 0);
  g_assert (partial < G_N_ELEMENTS (partial_blocks));

  g_string_append (str, cli->progress_msg);
  g_string_append (str, " ");

  if (flatpak_fancy_output ())
    g_string_append (str, FLATPAK_ANSI_FAINT_ON);

  for (i = 0; i < n_full; i++)
    g_string_append (str, full_block);

  if (i < bar_length)
    {
      g_string_append (str, partial_blocks[partial]);
      i++;
    }

  if (flatpak_fancy_output ())
    g_string_append (str, FLATPAK_ANSI_FAINT_OFF);

  for (; i < bar_length; i++)
    g_string_append (str, " ");

  g_string_append (str, " ");
  /* Download progress percentage, use the appropriate
    percent format for your language */
  g_string_append_printf (str, _("%3d%%"), percent);

  if (speed)
    g_string_append_printf (str, "  %s", speed);

  if (flatpak_fancy_output ())
    {
      flatpak_table_printer_set_cell (cli->printer, cli->progress_row, 0, str->str);
      if (flatpak_transaction_operation_get_operation_type (op) != FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
        {
          g_autofree char *formatted_max = NULL;
          g_autofree char *formatted = NULL;
          g_autofree char *text = NULL;
          int row;

          // avoid "bytes"
          formatted = transferred < 1000 ? g_format_size (1000) : g_format_size (transferred);
          formatted_max = max < 1000 ? g_format_size (1000) : g_format_size (max);

          text = g_strdup_printf ("%s / %s", formatted, formatted_max);
          row = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (op), "row"));
          flatpak_table_printer_set_decimal_cell (cli->printer, row, cli->download_col, text);
        }
      if (!redraw (cli))
        g_print ("\r%s", str->str); /* redraw failed, just update the progress */
      flatpak_pty_set_progress (percent);
    }
  else
    g_print ("%s\n", str->str);
}

static void
set_progress (FlatpakCliTransaction *self,
              const char            *text)
{
  flatpak_table_printer_set_cell (self->printer, self->progress_row, 0, text);
}

static void
new_operation (FlatpakTransaction          *transaction,
               FlatpakTransactionOperation *op,
               FlatpakTransactionProgress  *progress)
{
  FlatpakCliTransaction *self = FLATPAK_CLI_TRANSACTION (transaction);
  FlatpakTransactionOperationType op_type = flatpak_transaction_operation_get_operation_type (op);
  g_autofree char *text = NULL;

  self->op++;
  self->op_progress = 0;

  switch (op_type)
    {
    case FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE:
    case FLATPAK_TRANSACTION_OPERATION_INSTALL:
      if (self->n_ops == 1)
        text = g_strdup (_("Installing…"));
      else
        text = g_strdup_printf (_("Installing %d/%d…"), self->op, self->n_ops);
      break;

    case FLATPAK_TRANSACTION_OPERATION_UPDATE:
      if (self->n_ops == 1)
        text = g_strdup (_("Updating…"));
      else
        text = g_strdup_printf (_("Updating %d/%d…"), self->op, self->n_ops);
      break;

    case FLATPAK_TRANSACTION_OPERATION_UNINSTALL:
      if (self->n_ops == 1)
        text = g_strdup (_("Uninstalling…"));
      else
        text = g_strdup_printf (_("Uninstalling %d/%d…"), self->op, self->n_ops);
      break;

    default:
      g_assert_not_reached ();
      break;
    }

  if (flatpak_fancy_output ())
    {
      set_progress (self, text);
      spin_op_progress (self, op);
      redraw (self);
    }
  else
    g_print ("%s\n", text);

  g_free (self->progress_msg);
  self->progress_msg = g_steal_pointer (&text);

  g_signal_connect (progress, "changed", G_CALLBACK (progress_changed_cb), self);
  flatpak_transaction_progress_set_update_frequency (progress, FLATPAK_CLI_UPDATE_INTERVAL_MS);
}

static void
operation_done (FlatpakTransaction          *transaction,
                FlatpakTransactionOperation *op,
                const char                  *commit,
                FlatpakTransactionResult     details)
{
  FlatpakCliTransaction *self = FLATPAK_CLI_TRANSACTION (transaction);
  FlatpakTransactionOperationType op_type = flatpak_transaction_operation_get_operation_type (op);

  if (op_type == FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
    set_op_progress (self, op, FLATPAK_ANSI_GREEN "-" FLATPAK_ANSI_COLOR_RESET);
  else
    set_op_progress (self, op, FLATPAK_ANSI_GREEN "✓" FLATPAK_ANSI_COLOR_RESET);

  if (flatpak_fancy_output ())
    redraw (self);
}

static gboolean
operation_error (FlatpakTransaction            *transaction,
                 FlatpakTransactionOperation   *op,
                 const GError                  *error,
                 FlatpakTransactionErrorDetails detail)
{
  FlatpakCliTransaction *self = FLATPAK_CLI_TRANSACTION (transaction);
  FlatpakTransactionOperationType op_type = flatpak_transaction_operation_get_operation_type (op);
  const char *ref = flatpak_transaction_operation_get_ref (op);
  g_autoptr(FlatpakRef) rref = flatpak_ref_parse (ref, NULL);
  gboolean non_fatal = (detail & FLATPAK_TRANSACTION_ERROR_DETAILS_NON_FATAL) != 0;
  g_autofree char *text = NULL;
  const char *on = "";
  const char *off = "";

  if (flatpak_fancy_output ())
    {
      on = FLATPAK_ANSI_BOLD_ON;
      off = FLATPAK_ANSI_BOLD_OFF;
    }

  if (g_error_matches (error, FLATPAK_ERROR, FLATPAK_ERROR_SKIPPED))
    {
      set_op_progress (self, op, "⍻");
      text = g_strdup_printf (_("Info: %s was skipped"), flatpak_ref_get_name (rref));
      if (flatpak_fancy_output ())
        {
          flatpak_table_printer_set_cell (self->printer, self->progress_row, 0, text);
          self->progress_row++;
          flatpak_table_printer_add_span (self->printer, "");
          flatpak_table_printer_finish_row (self->printer);
          redraw (self);
        }
      else
        g_print ("%s\n", text);

      return TRUE;
    }

  set_op_progress (self, op, "✗");

  /* Here we go to great lengths not to split the sentences. See
   * https://wiki.gnome.org/TranslationProject/DevGuidelines/Never%20split%20sentences
   */
  if (g_error_matches (error, FLATPAK_ERROR, FLATPAK_ERROR_ALREADY_INSTALLED))
    {
      if (non_fatal)
        text = g_strdup_printf (_("Warning: %s%s%s already installed"),
                                on, flatpak_ref_get_name (rref), off);
      else
        text = g_strdup_printf (_("Error: %s%s%s already installed"),
                                on, flatpak_ref_get_name (rref), off);
    }
  else if (g_error_matches (error, FLATPAK_ERROR, FLATPAK_ERROR_NOT_INSTALLED))
    {
      if (non_fatal)
        text = g_strdup_printf (_("Warning: %s%s%s not installed"),
                                on, flatpak_ref_get_name (rref), off);
      else
        text = g_strdup_printf (_("Error: %s%s%s not installed"),
                                on, flatpak_ref_get_name (rref), off);
    }
  else if (g_error_matches (error, FLATPAK_ERROR, FLATPAK_ERROR_NEED_NEW_FLATPAK))
    {
      if (non_fatal)
        text = g_strdup_printf (_("Warning: %s%s%s needs a later flatpak version"),
                                on, flatpak_ref_get_name (rref), off);
      else
        text = g_strdup_printf (_("Error: %s%s%s needs a later flatpak version"),
                                on, flatpak_ref_get_name (rref), off);
    }
  else if (g_error_matches (error, FLATPAK_ERROR, FLATPAK_ERROR_OUT_OF_SPACE))
    {
      if (non_fatal)
        text = g_strdup (_("Warning: Not enough disk space to complete this operation"));
      else
        text = g_strdup (_("Error: Not enough disk space to complete this operation"));
    }
  else if (error)
    {
      if (non_fatal)
        text = g_strdup_printf (_("Warning: %s"), error->message);
      else
        text = g_strdup_printf (_("Error: %s"), error->message);
    }
  else
    text = g_strdup ("(internal error, please report)");

  if (!non_fatal && self->first_operation_error == NULL)
    {
      /* Here we go to great lengths not to split the sentences. See
       * https://wiki.gnome.org/TranslationProject/DevGuidelines/Never%20split%20sentences
       */
      switch (op_type)
        {
          case FLATPAK_TRANSACTION_OPERATION_INSTALL:
            g_propagate_prefixed_error (&self->first_operation_error,
                                        g_error_copy (error),
                                        _("Failed to install %s%s%s: "),
                                        on, flatpak_ref_get_name (rref), off);
            break;

          case FLATPAK_TRANSACTION_OPERATION_UPDATE:
            g_propagate_prefixed_error (&self->first_operation_error,
                                        g_error_copy (error),
                                        _("Failed to update %s%s%s: "),
                                        on, flatpak_ref_get_name (rref), off);
            break;

          case FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE:
            g_propagate_prefixed_error (&self->first_operation_error,
                                        g_error_copy (error),
                                        _("Failed to install bundle %s%s%s: "),
                                        on, flatpak_ref_get_name (rref), off);
            break;

          case FLATPAK_TRANSACTION_OPERATION_UNINSTALL:
            g_propagate_prefixed_error (&self->first_operation_error,
                                        g_error_copy (error),
                                        _("Failed to uninstall %s%s%s: "),
                                        on, flatpak_ref_get_name (rref), off);
            break;

          default:
            g_assert_not_reached ();
        }
    }

  /* On a fatal error, just clear the progress line. The error will be printed in main() before exiting. */
  if (!non_fatal && self->stop_on_first_error)
    {
      if (flatpak_fancy_output ())
        {
          flatpak_table_printer_set_cell (self->printer, self->progress_row, 0, "");
          redraw (self);
        }

      return FALSE;
    }

  if (flatpak_fancy_output ())
    {
      flatpak_table_printer_set_cell (self->printer, self->progress_row, 0, text);
      self->progress_row++;
      flatpak_table_printer_add_span (self->printer, "");
      flatpak_table_printer_finish_row (self->printer);
      redraw (self);
    }
  else
    g_printerr ("%s\n", text);

  return TRUE; /* Continue */
}

static gboolean
webflow_start (FlatpakTransaction *transaction,
               const char         *remote,
               const char         *url,
               GVariant           *options,
               guint               id)
{
  FlatpakCliTransaction *self = FLATPAK_CLI_TRANSACTION (transaction);
  const char *browser;
  g_autoptr(GError) local_error = NULL;
  const char *args[3] = { NULL, url, NULL };

  self->did_interaction = TRUE;

  if (!self->disable_interaction)
    {
      g_print (_("Authentication required for remote '%s'\n"), remote);
      if (!flatpak_yes_no_prompt (TRUE, _("Open browser?")))
        return FALSE;
    }

  /* Allow hard overrides with $BROWSER */
  browser = g_getenv ("BROWSER");
  if (browser != NULL)
    {
      args[0] = browser;
      if (!g_spawn_async (NULL, (char **)args, NULL, G_SPAWN_SEARCH_PATH,
                          NULL, NULL, NULL, &local_error))
        {
          g_printerr ("Failed to start browser %s: %s\n", browser, local_error->message);
          return FALSE;
        }
    }
  else
    {
      if (!g_app_info_launch_default_for_uri (url, NULL, &local_error))
        {
          g_printerr ("Failed to show url: %s\n", local_error->message);
          return FALSE;
        }
    }

  g_print ("Waiting for browser...\n");

  return TRUE;
}

static void
webflow_done (FlatpakTransaction *transaction,
              GVariant           *options,
              guint               id)
{
  g_print ("Browser done\n");
}

static gboolean
basic_auth_start (FlatpakTransaction *transaction,
                  const char         *remote,
                  const char         *realm,
                  GVariant           *options,
                  guint               id)
{
  FlatpakCliTransaction *self = FLATPAK_CLI_TRANSACTION (transaction);
  char *user, *password, *previous_error = NULL;

  if (self->disable_interaction)
    return FALSE;

  self->did_interaction = TRUE;

  if (g_variant_lookup (options, "previous-error", "&s", &previous_error))
    g_print ("%s\n", previous_error);

  g_print (_("Login required remote %s (realm %s)\n"), remote, realm);
  user = flatpak_prompt (FALSE, _("User"));
  if (user == NULL)
    return FALSE;

  password = flatpak_password_prompt (_("Password"));
  if (password == NULL)
    return FALSE;

  flatpak_transaction_complete_basic_auth (transaction, id, user, password, NULL);
  return TRUE;
}


typedef enum {
  EOL_UNDECIDED,
  EOL_IGNORE,        /* Don't do anything, we already printed a warning */
  EOL_NO_REBASE,     /* Choose to not rebase */
  EOL_REBASE,        /* Choose to rebase */
} EolAction;

static void
print_eol_info_message (FlatpakDir        *dir,
                        FlatpakDecomposed *ref,
                        const char        *ref_name,
                        const char        *rebased_to_ref,
                        const char        *reason)
{
  gboolean is_pinned = flatpak_dir_ref_is_pinned (dir, flatpak_decomposed_get_ref (ref));
  g_autofree char *ref_branch = flatpak_decomposed_dup_branch (ref);
  const char *on = "";
  const char *off = "";

  if (flatpak_fancy_output ())
    {
      on = FLATPAK_ANSI_BOLD_ON;
      off = FLATPAK_ANSI_BOLD_OFF;
    }

  /* Here we go to great lengths not to split the sentences. See
   * https://wiki.gnome.org/TranslationProject/DevGuidelines/Never%20split%20sentences
   */
  if (rebased_to_ref)
    {
      g_autoptr(FlatpakDecomposed) eolr_decomposed = NULL;
      g_autofree char *eolr_name = NULL;
      const char *eolr_branch;

      eolr_decomposed = flatpak_decomposed_new_from_ref (rebased_to_ref, NULL);

      /* These are guarantees from FlatpakTransaction */
      g_assert (eolr_decomposed != NULL);
      g_assert (flatpak_decomposed_get_kind (ref) == flatpak_decomposed_get_kind (eolr_decomposed));

      eolr_name = flatpak_decomposed_dup_id (eolr_decomposed);
      eolr_branch = flatpak_decomposed_get_branch (eolr_decomposed);

      if (is_pinned)
        {
          /* Only runtimes can be pinned */
          g_print (_("\nInfo: (pinned) runtime %s%s%s branch %s%s%s is end-of-life, in favor of %s%s%s branch %s%s%s\n"),
                   on, ref_name, off, on, ref_branch, off, on, eolr_name, off, on, eolr_branch, off);
        }
      else
        {
          if (flatpak_decomposed_is_runtime (ref))
            g_print (_("\nInfo: runtime %s%s%s branch %s%s%s is end-of-life, in favor of %s%s%s branch %s%s%s\n"),
                     on, ref_name, off, on, ref_branch, off, on, eolr_name, off, on, eolr_branch, off);
          else
            g_print (_("\nInfo: app %s%s%s branch %s%s%s is end-of-life, in favor of %s%s%s branch %s%s%s\n"),
                     on, ref_name, off, on, ref_branch, off, on, eolr_name, off, on, eolr_branch, off);
        }
    }
  else if (reason)
    {
      g_autofree char *escaped_reason = flatpak_escape_string (reason,
                                                               FLATPAK_ESCAPE_ALLOW_NEWLINES |
                                                               FLATPAK_ESCAPE_DO_NOT_QUOTE);
      if (is_pinned)
        {
          /* Only runtimes can be pinned */
          g_print (_("\nInfo: (pinned) runtime %s%s%s branch %s%s%s is end-of-life, with reason:\n"),
                   on, ref_name, off, on, ref_branch, off);
        }
      else
        {
          if (flatpak_decomposed_is_runtime (ref))
            g_print (_("\nInfo: runtime %s%s%s branch %s%s%s is end-of-life, with reason:\n"),
                     on, ref_name, off, on, ref_branch, off);
          else
            g_print (_("\nInfo: app %s%s%s branch %s%s%s is end-of-life, with reason:\n"),
                     on, ref_name, off, on, ref_branch, off);
        }
      g_print ("   %s\n", escaped_reason);
    }
}

static void
check_current_transaction_for_dependent_apps (GPtrArray          *apps,
                                              FlatpakTransaction *transaction,
                                              FlatpakDecomposed  *ref)
{
  g_autoptr(FlatpakTransactionOperation) ref_op = NULL;
  GPtrArray *related_ops;

  ref_op = flatpak_transaction_get_operation_for_ref (transaction, NULL, flatpak_decomposed_get_ref (ref), NULL);
  g_assert (ref_op != NULL);

  /* Get the related ops to find any apps that use @ref as a runtime or extension */
  related_ops = flatpak_transaction_operation_get_related_to_ops (ref_op);
  if (related_ops == NULL)
    return;

  for (int i = 0; i < related_ops->len; i++)
    {
      FlatpakTransactionOperation *related_op = g_ptr_array_index (related_ops, i);
      const char *related_op_ref = flatpak_transaction_operation_get_ref (related_op);
      g_autoptr(FlatpakDecomposed) related_op_decomposed = flatpak_decomposed_new_from_ref (related_op_ref, NULL);

      if (related_op_decomposed == NULL)
        continue;
      if (flatpak_decomposed_id_is_subref (related_op_decomposed))
        continue;

      /* Recurse in case @ref was a runtime extension. We need to check since a
       * runtime can have a runtime extension in its related ops in the
       * extra-data case, so if we recurse unconditionally it could be infinite
       * recursion.
       */
      if (flatpak_decomposed_is_runtime (related_op_decomposed))
        {
          GKeyFile *metadata = flatpak_transaction_operation_get_metadata (ref_op);
          if (g_key_file_has_group (metadata, FLATPAK_METADATA_GROUP_EXTENSION_OF))
            check_current_transaction_for_dependent_apps (apps, transaction, related_op_decomposed);
        }
      else if (!g_ptr_array_find_with_equal_func (apps, related_op_decomposed, (GEqualFunc)flatpak_decomposed_equal, NULL))
        g_ptr_array_add (apps, g_steal_pointer (&related_op_decomposed));
    }
}

static GPtrArray *
find_reverse_dep_apps (FlatpakTransaction *transaction,
                       FlatpakDir         *dir,
                       FlatpakDecomposed  *ref,
                       gboolean           *out_is_extension)
{
  FlatpakCliTransaction *self = FLATPAK_CLI_TRANSACTION (transaction);
  g_autoptr(GPtrArray) apps = NULL;
  g_autoptr(GError) local_error = NULL;

  g_assert (out_is_extension);

  *out_is_extension = flatpak_dir_is_runtime_extension (dir, ref);
  if (*out_is_extension)
    {
      /* Find apps which are using the ref as an extension directly or as an
       * extension of their runtime.
       */
      apps = flatpak_dir_list_app_refs_with_runtime_extension (dir,
                                                               &self->runtime_app_map,
                                                               &self->extension_app_map,
                                                               ref, NULL, &local_error);
      if (apps == NULL)
        {
          g_info ("Unable to list apps using extension %s: %s\n",
                  flatpak_decomposed_get_ref (ref), local_error->message);
          return NULL;
        }
    }
  else
    {
      /* Find any apps using the runtime directly */
      apps = flatpak_dir_list_app_refs_with_runtime (dir, &self->runtime_app_map, ref,
                                                     NULL, &local_error);
      if (apps == NULL)
        {
          g_info ("Unable to find apps using runtime %s: %s\n",
                  flatpak_decomposed_get_ref (ref), local_error->message);
          return NULL;
        }
    }

  /* Also check the current transaction since it's possible the EOL ref
   * and/or any app(s) that depend on it are not installed. It's also
   * possible the current transaction updates one of the apps to a
   * newer runtime but we don't handle that yet
   * (https://github.com/flatpak/flatpak/issues/4832)
   */
  check_current_transaction_for_dependent_apps (apps, transaction, ref);

  return g_steal_pointer (&apps);
}

static gboolean
end_of_lifed_with_rebase (FlatpakTransaction *transaction,
                          const char         *remote,
                          const char         *ref_str,
                          const char         *reason,
                          const char         *rebased_to_ref,
                          const char        **previous_ids)
{
  FlatpakCliTransaction *self = FLATPAK_CLI_TRANSACTION (transaction);
  g_autoptr(FlatpakDecomposed) ref = flatpak_decomposed_new_from_ref (ref_str, NULL);
  g_autofree char *name = NULL;
  EolAction action = EOL_UNDECIDED;
  EolAction old_action = EOL_UNDECIDED;
  gboolean can_rebase = rebased_to_ref != NULL && remote != NULL;
  g_autoptr(FlatpakInstallation) installation = flatpak_transaction_get_installation (transaction);
  g_autoptr(FlatpakDir) dir = flatpak_installation_get_dir (installation, NULL);

  if (ref == NULL)
    return FALSE; /* Shouldn't happen, the ref should be valid */

  name = flatpak_decomposed_dup_id (ref);

  self->did_interaction = TRUE;

  if (flatpak_decomposed_id_is_subref (ref))
    {
      GLNX_HASH_TABLE_FOREACH_KV (self->eol_actions, FlatpakDecomposed *, eoled_ref, gpointer, value)
        {
          guint old_eol_action = GPOINTER_TO_UINT (value);

          if (flatpak_decomposed_id_is_subref_of (ref, eoled_ref))
              {
                old_action = old_eol_action; /* Do the same */
                break;
              }
        }
    }

  if (old_action != EOL_UNDECIDED)
    {
      switch (old_action)
        {
        default:
        case EOL_IGNORE:
          if (!can_rebase)
            action = EOL_IGNORE;
          /* Else, ask if we want to rebase */
          break;
        case EOL_REBASE:
        case EOL_NO_REBASE:
          if (can_rebase)
            action = old_action;
          else
            action = EOL_IGNORE;
        }
    }

  if (action == EOL_UNDECIDED)
    {
      action = EOL_IGNORE;

      print_eol_info_message (dir, ref, name, rebased_to_ref, reason);

      if (flatpak_decomposed_is_runtime (ref) && !rebased_to_ref)
        {
          gboolean is_extension;
          g_autoptr(GPtrArray) apps = find_reverse_dep_apps (transaction, dir, ref, &is_extension);

          if (apps && apps->len > 0)
            {
              if (is_extension)
                g_print (_("Info: applications using this extension:\n"));
              else
                g_print (_("Info: applications using this runtime:\n"));

              g_print ("   ");
              for (guint i = 0; i < apps->len; i++)
                {
                  FlatpakDecomposed *app_ref = g_ptr_array_index (apps, i);
                  g_autofree char *id = flatpak_decomposed_dup_id (app_ref);
                  if (i != 0)
                    g_print (", ");
                  g_print ("%s", id);
                }
              g_print ("\n");
            }
        }

      if (rebased_to_ref && remote)
        {
          /* The context for this prompt is in print_eol_info_message() */
          if (self->disable_interaction ||
              flatpak_yes_no_prompt (TRUE, _("Replace?")))
            {
              if (self->disable_interaction)
                g_print (_("Updating to rebased version\n"));

              action = EOL_REBASE;
            }
          else
            action = EOL_NO_REBASE;
        }
    }
  else
    {
      g_info ("%s is end-of-life, using action from parent ref", name);
    }

  /* Cache for later comparison and reuse */
  g_hash_table_insert (self->eol_actions, flatpak_decomposed_ref (ref), GUINT_TO_POINTER (action));

  if (action == EOL_REBASE)
    {
      g_autoptr(GError) error = NULL;

      if (!flatpak_transaction_add_rebase_and_uninstall (transaction, remote, rebased_to_ref, ref_str, NULL, previous_ids, &error))
        {
          g_propagate_prefixed_error (&self->first_operation_error,
                                      g_error_copy (error),
                                      _("Failed to rebase %s to %s: "),
                                      name, rebased_to_ref);
          return FALSE;
        }

      return TRUE; /* skip install/update op of end-of-life ref */
    }
  else /* IGNORE or NO_REBASE */
    return FALSE;
}

static int
cmpstringp (const void *p1, const void *p2)
{
  return strcmp (*(char * const *) p1, *(char * const *) p2);
}

static void
append_permissions (GPtrArray  *permissions,
                    GKeyFile   *metadata,
                    GKeyFile   *old_metadata,
                    const char *group)
{
  g_auto(GStrv) options = g_key_file_get_string_list (metadata, FLATPAK_METADATA_GROUP_CONTEXT, group, NULL, NULL);
  g_auto(GStrv) old_options = NULL;
  int i;

  if (options == NULL)
    return;

  qsort (options, g_strv_length (options), sizeof (const char *), cmpstringp);

  if (old_metadata)
    old_options = g_key_file_get_string_list (old_metadata, FLATPAK_METADATA_GROUP_CONTEXT, group, NULL, NULL);

  for (i = 0; options[i] != NULL; i++)
    {
      const char *option = options[i];
      if (option[0] == '!')
        continue;

      if (old_options && g_strv_contains ((const char * const *) old_options, option))
        continue;

      if (strcmp (group, FLATPAK_METADATA_KEY_DEVICES) == 0 && strcmp (option, "all") == 0)
        option = "devices";

      g_ptr_array_add (permissions, g_strdup (option));
    }
}

static void
append_bus (GPtrArray  *talk,
            GPtrArray  *own,
            GKeyFile   *metadata,
            GKeyFile   *old_metadata,
            const char *group)
{
  g_auto(GStrv) keys = NULL;
  gsize i, keys_count;

  keys = g_key_file_get_keys (metadata, group, &keys_count, NULL);
  if (keys == NULL)
    return;

  qsort (keys, g_strv_length (keys), sizeof (const char *), cmpstringp);

  for (i = 0; i < keys_count; i++)
    {
      const char *key = keys[i];
      g_autofree char *value = g_key_file_get_string (metadata, group, key, NULL);

      if (g_strcmp0 (value, "none") == 0)
        continue;

      if (old_metadata)
        {
          g_autofree char *old_value = g_key_file_get_string (old_metadata, group, key, NULL);
          if (g_strcmp0 (old_value, value) == 0)
            continue;
        }

      if (g_strcmp0 (value, "own") == 0)
        g_ptr_array_add (own, g_strdup (key));
      else
        g_ptr_array_add (talk, g_strdup (key));
    }
}

static void
append_usb (GPtrArray *usb_array,
            GKeyFile  *metadata,
            GKeyFile  *old_metadata)
{
  gsize size = 0;
  g_auto(GStrv) hidden_devices = NULL;
  g_auto(GStrv) old_hidden_devices = NULL;
  g_auto(GStrv) old_enumerables = NULL;
  g_auto(GStrv) enumerables = NULL;

  enumerables = g_key_file_get_string_list (metadata,
                                            FLATPAK_METADATA_GROUP_USB_DEVICES,
                                            FLATPAK_METADATA_KEY_USB_ENUMERABLE_DEVICES,
                                            &size, NULL);

  if (old_metadata)
    old_enumerables = g_key_file_get_string_list (old_metadata,
                                                  FLATPAK_METADATA_GROUP_USB_DEVICES,
                                                  FLATPAK_METADATA_KEY_USB_ENUMERABLE_DEVICES,
                                                  NULL, NULL);

  for (size_t i = 0; i < size; i++)
    {
      const char *enumerable = enumerables[i];
      if (old_enumerables == NULL || !g_strv_contains ((const char * const *) old_enumerables, enumerable))
        g_ptr_array_add (usb_array, g_strdup (enumerable));
    }

  size = 0;

  hidden_devices = g_key_file_get_string_list (metadata,
                                               FLATPAK_METADATA_GROUP_USB_DEVICES,
                                               FLATPAK_METADATA_KEY_USB_HIDDEN_DEVICES,
                                               &size, NULL);

  if (old_metadata)
    old_hidden_devices = g_key_file_get_string_list (old_metadata,
                                                     FLATPAK_METADATA_GROUP_USB_DEVICES,
                                                     FLATPAK_METADATA_KEY_USB_HIDDEN_DEVICES,
                                                     NULL, NULL);

  for (size_t i = 0; i < size; i++)
    {
      const char *hidden = hidden_devices[i];
      if (old_hidden_devices == NULL || !g_strv_contains ((const char * const *) old_hidden_devices, hidden))
        g_ptr_array_add (usb_array, g_strdup_printf ("!%s", hidden));
    }
}

static void
append_tags (GPtrArray *tags_array,
             GKeyFile  *metadata,
             GKeyFile  *old_metadata)
{
  gsize i, size = 0;
  g_auto(GStrv) tags = g_key_file_get_string_list (metadata, FLATPAK_METADATA_GROUP_APPLICATION, "tags",
                                                   &size, NULL);
  g_auto(GStrv) old_tags = NULL;

  if (old_metadata)
    old_tags = g_key_file_get_string_list (old_metadata, FLATPAK_METADATA_GROUP_APPLICATION, "tags",
                                           NULL, NULL);

  for (i = 0; i < size; i++)
    {
      const char *tag = tags[i];
      if (old_tags == NULL || !g_strv_contains ((const char * const *) old_tags, tag))
        g_ptr_array_add (tags_array, g_strdup (tag));
    }
}

static void
print_perm_line (int        idx,
                 GPtrArray *items,
                 int        cols)
{
  g_autoptr(GString) res = g_string_new (NULL);
  g_autofree char *escaped_first_perm = NULL;
  int i;

  escaped_first_perm = flatpak_escape_string (items->pdata[0], FLATPAK_ESCAPE_DEFAULT);
  g_string_append_printf (res, "    [%d] %s", idx, escaped_first_perm);

  for (i = 1; i < items->len; i++)
    {
      g_autofree char *escaped = flatpak_escape_string (items->pdata[i],
                                                        FLATPAK_ESCAPE_DEFAULT);
      char *p;
      int len;

      p = strrchr (res->str, '\n');
      if (!p)
        p = res->str;

      len = (res->str + strlen (res->str)) - p;
      if (len + strlen (escaped) + 2 >= cols)
        g_string_append_printf (res, ",\n        %s", escaped);
      else
        g_string_append_printf (res, ", %s", escaped);
    }

  g_print ("%s\n", res->str);
}

static void
print_permissions (FlatpakCliTransaction *self,
                   const char            *ref,
                   GKeyFile              *metadata,
                   GKeyFile              *old_metadata)
{
  g_autoptr(FlatpakRef) rref = flatpak_ref_parse (ref, NULL);
  g_autoptr(GPtrArray) permissions = g_ptr_array_new_with_free_func (g_free);
  g_autoptr(GPtrArray) files = g_ptr_array_new_with_free_func (g_free);
  g_autoptr(GPtrArray) usb = g_ptr_array_new_with_free_func (g_free);
  g_autoptr(GPtrArray) session_bus_talk = g_ptr_array_new_with_free_func (g_free);
  g_autoptr(GPtrArray) session_bus_own = g_ptr_array_new_with_free_func (g_free);
  g_autoptr(GPtrArray) system_bus_talk = g_ptr_array_new_with_free_func (g_free);
  g_autoptr(GPtrArray) system_bus_own = g_ptr_array_new_with_free_func (g_free);
  g_autoptr(GPtrArray) tags = g_ptr_array_new_with_free_func (g_free);
  g_autoptr(FlatpakTablePrinter) printer = NULL;
  int max_permission_width;
  int n_permission_cols;
  int i, j;
  int rows, cols;
  int table_rows, table_cols;
  const char *on = "";
  const char *off = "";

  if (flatpak_fancy_output ())
    {
      on = FLATPAK_ANSI_BOLD_ON;
      off = FLATPAK_ANSI_BOLD_OFF;
    }

  if (metadata == NULL)
    return;

  /* Only apps have permissions */
  if (flatpak_ref_get_kind (rref) != FLATPAK_REF_KIND_APP)
    return;

  append_permissions (permissions, metadata, old_metadata, FLATPAK_METADATA_KEY_SHARED);
  append_permissions (permissions, metadata, old_metadata, FLATPAK_METADATA_KEY_SOCKETS);
  append_permissions (permissions, metadata, old_metadata, FLATPAK_METADATA_KEY_DEVICES);
  append_permissions (permissions, metadata, old_metadata, FLATPAK_METADATA_KEY_FEATURES);
  append_permissions (files, metadata, old_metadata, FLATPAK_METADATA_KEY_FILESYSTEMS);
  append_usb (usb, metadata, old_metadata);
  append_bus (session_bus_talk, session_bus_own,
              metadata, old_metadata, FLATPAK_METADATA_GROUP_SESSION_BUS_POLICY);
  append_bus (system_bus_talk, system_bus_own,
              metadata, old_metadata, FLATPAK_METADATA_GROUP_SYSTEM_BUS_POLICY);
  append_tags (tags, metadata, old_metadata);

  j = 1;
  if (files->len > 0)
    g_ptr_array_add (permissions, g_strdup_printf ("file access [%d]", j++));
  if (session_bus_talk->len > 0)
    g_ptr_array_add (permissions, g_strdup_printf ("dbus access [%d]", j++));
  if (session_bus_own->len > 0)
    g_ptr_array_add (permissions, g_strdup_printf ("bus ownership [%d]", j++));
  if (system_bus_talk->len > 0)
    g_ptr_array_add (permissions, g_strdup_printf ("system dbus access [%d]", j++));
  if (system_bus_own->len > 0)
    g_ptr_array_add (permissions, g_strdup_printf ("system bus ownership [%d]", j++));
  if (usb->len > 0)
    g_ptr_array_add (permissions, g_strdup_printf ("USB portal access [%d]", j++));
  if (tags->len > 0)
    g_ptr_array_add (permissions, g_strdup_printf ("tags [%d]", j++));

  /* Early exit if no (or no new) permissions */
  if (permissions->len == 0)
    return;

  g_print ("\n");

  if (old_metadata)
    g_print (_("New %s%s%s permissions:"), on, flatpak_ref_get_name (rref), off);
  else
    g_print (_("%s%s%s permissions:"), on, flatpak_ref_get_name (rref), off);

  g_print ("\n");

  flatpak_get_window_size (&rows, &cols);
  max_permission_width = 0;
  for (i = 0; i < permissions->len; i++)
    max_permission_width = MAX (max_permission_width, strlen (g_ptr_array_index (permissions, i)));

  /* At least 4 columns, but more if we're guaranteed to fit */
  n_permission_cols =  MAX (4, cols / (max_permission_width + 4));

  printer = flatpak_table_printer_new ();
  for (i = 0; i < permissions->len; i++)
    {
      char *perm = g_ptr_array_index (permissions, i);
      if (i % n_permission_cols == 0)
        {
          g_autofree char *text = NULL;

          if (i > 0)
            flatpak_table_printer_finish_row (printer);

          text = g_strdup_printf ("    %s", perm);
          flatpak_table_printer_add_column (printer, text);
        }
      else
        flatpak_table_printer_add_column (printer, perm);
    }
  flatpak_table_printer_finish_row (printer);

  for (i = 0; i < n_permission_cols; i++)
    flatpak_table_printer_set_column_expand (printer, i, TRUE);

  flatpak_table_printer_print_full (printer, 0, cols, &table_rows, &table_cols);

  g_print ("\n\n");

  j = 1;
  if (files->len > 0)
    print_perm_line (j++, files, cols);
  if (session_bus_talk->len > 0)
    print_perm_line (j++, session_bus_talk, cols);
  if (session_bus_own->len > 0)
    print_perm_line (j++, session_bus_own, cols);
  if (system_bus_talk->len > 0)
    print_perm_line (j++, system_bus_talk, cols);
  if (system_bus_own->len > 0)
    print_perm_line (j++, system_bus_own, cols);
  if (usb->len > 0)
    print_perm_line (j++, usb, cols);
  if (tags->len > 0)
    print_perm_line (j++, tags, cols);
}

static void
message_handler (const gchar   *log_domain,
                 GLogLevelFlags log_level,
                 const gchar   *message,
                 gpointer       user_data)
{
  FlatpakCliTransaction *self = FLATPAK_CLI_TRANSACTION (user_data);
  g_autofree char *text = NULL;

  text = g_strconcat (_("Warning: "), message, NULL);

  if (flatpak_fancy_output ())
    {
      flatpak_table_printer_set_cell (self->printer, self->progress_row, 0, text);
      self->progress_row++;
      flatpak_table_printer_add_span (self->printer, "");
      flatpak_table_printer_finish_row (self->printer);
      redraw (self);
    }
  else
    g_print ("%s\n", text);
}

static gboolean
transaction_ready_pre_auth (FlatpakTransaction *transaction)
{
  FlatpakCliTransaction *self = FLATPAK_CLI_TRANSACTION (transaction);
  g_autolist(FlatpakTransactionOperation) ops = flatpak_transaction_get_operations (transaction);
  GList *l;
  int i;
  FlatpakTablePrinter *printer;
  const char *op_shorthand[] = { "i", "u", "i", "r", "i" };

  /* These caches may no longer be valid once the transaction runs */
  g_clear_pointer (&self->runtime_app_map, g_hash_table_unref);
  g_clear_pointer (&self->extension_app_map, g_hash_table_unref);

  if (ops == NULL)
    return TRUE;

  self->n_ops = g_list_length (ops);

  for (l = ops; l != NULL; l = l->next)
    {
      FlatpakTransactionOperation *op = l->data;
      FlatpakTransactionOperationType type = flatpak_transaction_operation_get_operation_type (op);

      switch (type)
        {
        case FLATPAK_TRANSACTION_OPERATION_UNINSTALL:
          self->uninstalling = TRUE;
          break;

        case FLATPAK_TRANSACTION_OPERATION_INSTALL:
        case FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE:
          self->installing = TRUE;
          break;

        case FLATPAK_TRANSACTION_OPERATION_UPDATE:
          self->updating = TRUE;
          break;

        default:;
        }
    }

  /* first, show permissions */
  for (l = ops; l != NULL; l = l->next)
    {
      FlatpakTransactionOperation *op = l->data;
      FlatpakTransactionOperationType type = flatpak_transaction_operation_get_operation_type (op);

      if (type == FLATPAK_TRANSACTION_OPERATION_INSTALL ||
          type == FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE ||
          type == FLATPAK_TRANSACTION_OPERATION_UPDATE)
        {
          const char *ref = flatpak_transaction_operation_get_ref (op);
          GKeyFile *metadata = flatpak_transaction_operation_get_metadata (op);
          GKeyFile *old_metadata = flatpak_transaction_operation_get_old_metadata (op);

          print_permissions (self, ref, metadata, old_metadata);
        }
    }

  g_print ("\n");

  printer = self->printer = flatpak_table_printer_new ();
  i = 0;

  flatpak_table_printer_set_column_title (printer, i++, "   ");
  flatpak_table_printer_set_column_title (printer, i++, "   ");

  flatpak_table_printer_set_column_expand (printer, i, TRUE);
  flatpak_table_printer_set_column_title (printer, i++, _("ID"));

  flatpak_table_printer_set_column_expand (printer, i, TRUE);
  if (!self->non_default_arch)
    {
      flatpak_table_printer_set_column_skip_unique (printer, i, TRUE);
      flatpak_table_printer_set_column_skip_unique_string (printer, i, flatpak_get_arch ());
    }
  flatpak_table_printer_set_column_title (printer, i++, _("Arch"));

  flatpak_table_printer_set_column_expand (printer, i, TRUE);
  flatpak_table_printer_set_column_title (printer, i++, _("Branch"));

  flatpak_table_printer_set_column_expand (printer, i, TRUE);
  /* translators: This is short for operation, the title of a one-char column */
  flatpak_table_printer_set_column_title (printer, i++, _("Op"));

  if (self->installing || self->updating)
    {
      g_autofree char *text1 = NULL;
      g_autofree char *text2 = NULL;
      g_autofree char *text = NULL;
      int size;

      flatpak_table_printer_set_column_expand (printer, i, TRUE);
      flatpak_table_printer_set_column_title (printer, i++, _("Remote"));
      self->download_col = i;

      /* Avoid resizing the download column too much,
       * by making the title as long as typical content
       */
      text1 = g_strdup_printf ("< 999.9 kB (%s)", _("partial"));
      text2 = g_strdup_printf ("  123.4 MB / 999.9 MB");
      size = MAX (strlen (text1), strlen (text2));
      text = g_strdup_printf ("%-*s", size, _("Download"));
      flatpak_table_printer_set_column_title (printer, i++, text);
    }

  for (l = ops, i = 1; l != NULL; l = l->next, i++)
    {
      FlatpakTransactionOperation *op = l->data;
      FlatpakTransactionOperationType type = flatpak_transaction_operation_get_operation_type (op);
      FlatpakDecomposed *ref = flatpak_transaction_operation_get_decomposed (op);
      const char *remote = flatpak_transaction_operation_get_remote (op);
      g_autofree char *id = flatpak_decomposed_dup_id (ref);
      const char *branch = flatpak_decomposed_get_branch (ref);
      g_autofree char *arch = flatpak_decomposed_dup_arch (ref);
      g_autofree char *rownum = g_strdup_printf ("%2d.", i);

      flatpak_table_printer_add_column (printer, rownum);
      flatpak_table_printer_add_column (printer, "   ");
      flatpak_table_printer_add_column (printer, id);
      flatpak_table_printer_add_column (printer, arch);
      flatpak_table_printer_add_column (printer, branch);
      flatpak_table_printer_add_column (printer, op_shorthand[type]);

      if (type == FLATPAK_TRANSACTION_OPERATION_INSTALL ||
          type == FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE ||
          type == FLATPAK_TRANSACTION_OPERATION_UPDATE)
        {
          guint64 download_size;
          g_autofree char *formatted = NULL;
          g_autofree char *text = NULL;
          const char *prefix;

          download_size = flatpak_transaction_operation_get_download_size (op);
          formatted = g_format_size (download_size);

          if (download_size > 0)
            prefix = "< ";
          else
            prefix = "";

          flatpak_table_printer_add_column (printer, remote);
          if (flatpak_transaction_operation_get_subpaths (op) != NULL)
            text = g_strdup_printf ("%s%s (%s)", prefix, formatted, _("partial"));
          else
            text = g_strdup_printf ("%s%s", prefix, formatted);
          flatpak_table_printer_add_decimal_column (printer, text);
        }

      g_object_set_data (G_OBJECT (op), "row", GINT_TO_POINTER (flatpak_table_printer_get_current_row (printer)));
      flatpak_table_printer_finish_row (printer);
    }

  flatpak_get_window_size (&self->rows, &self->cols);

  g_print ("\n");

  flatpak_table_printer_print_full (printer, 0, self->cols,
                                    &self->table_height, &self->table_width);

  g_print ("\n");

  if (!self->disable_interaction)
    {
      g_autoptr(FlatpakInstallation) installation = flatpak_transaction_get_installation (transaction);
      const char *name;
      const char *id;
      gboolean ret;

      g_print ("\n");

      name = flatpak_installation_get_display_name (installation);
      id = flatpak_installation_get_id (installation);

      if (flatpak_installation_get_is_user (installation))
        ret = flatpak_yes_no_prompt (TRUE, _("Proceed with these changes to the user installation?"));
      else if (g_strcmp0 (id, SYSTEM_DIR_DEFAULT_ID) == 0)
        ret = flatpak_yes_no_prompt (TRUE, _("Proceed with these changes to the system installation?"));
      else
        ret = flatpak_yes_no_prompt (TRUE, _("Proceed with these changes to the %s?"), name);

      if (!ret)
        return FALSE;
    }
  else
    g_print ("\n\n");

  self->did_interaction = FALSE;

  return TRUE;
}

static gboolean
transaction_ready (FlatpakTransaction *transaction)
{
  FlatpakCliTransaction *self = FLATPAK_CLI_TRANSACTION (transaction);
  GList *ops = flatpak_transaction_get_operations (transaction);
  GList *l;
  FlatpakTablePrinter *printer;

  if (ops == NULL)
    return TRUE;

  printer = self->printer;

  if (self->did_interaction)
    {
      /* We did some interaction since ready_pre_auth which messes up the formating, so re-print table */
      flatpak_table_printer_print_full (printer, 0, self->cols,
                                        &self->table_height, &self->table_width);
      g_print ("\n\n");
    }

  for (l = ops; l; l = l->next)
    {
      FlatpakTransactionOperation *op = l->data;
      set_op_progress (self, op, " ");
    }

  g_list_free_full (ops, g_object_unref);

  flatpak_table_printer_add_span (printer, "");
  flatpak_table_printer_finish_row (printer);
  flatpak_table_printer_add_span (printer, "");
  self->progress_row = flatpak_table_printer_get_current_row (printer);
  flatpak_table_printer_finish_row (printer);

  self->table_height += 3; /* 2 for the added lines and one for the newline from the user after the prompt */

  if (flatpak_fancy_output ())
    {
      flatpak_hide_cursor ();
      flatpak_enable_raw_mode ();
      redraw (self);
    }

  g_log_set_handler (G_LOG_DOMAIN, G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_WARNING, message_handler, transaction);

  return TRUE;
}

static void
flatpak_cli_transaction_finalize (GObject *object)
{
  FlatpakCliTransaction *self = FLATPAK_CLI_TRANSACTION (object);

  if (self->first_operation_error)
    g_error_free (self->first_operation_error);

  g_free (self->progress_msg);

  g_hash_table_unref (self->eol_actions);

  if (self->runtime_app_map)
    g_hash_table_unref (self->runtime_app_map);

  if (self->extension_app_map)
    g_hash_table_unref (self->extension_app_map);

  if (self->printer)
    flatpak_table_printer_free (self->printer);

  G_OBJECT_CLASS (flatpak_cli_transaction_parent_class)->finalize (object);
}

static void
flatpak_cli_transaction_init (FlatpakCliTransaction *self)
{
  self->eol_actions = g_hash_table_new_full ((GHashFunc)flatpak_decomposed_hash, (GEqualFunc)flatpak_decomposed_equal,
                                             (GDestroyNotify)flatpak_decomposed_unref, NULL);
}

static gboolean flatpak_cli_transaction_run (FlatpakTransaction *transaction,
                                             GCancellable       *cancellable,
                                             GError            **error);

static void
flatpak_cli_transaction_class_init (FlatpakCliTransactionClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  FlatpakTransactionClass *transaction_class = FLATPAK_TRANSACTION_CLASS (klass);

  object_class->finalize = flatpak_cli_transaction_finalize;
  transaction_class->add_new_remote = add_new_remote;
  transaction_class->ready = transaction_ready;
  transaction_class->ready_pre_auth = transaction_ready_pre_auth;
  transaction_class->new_operation = new_operation;
  transaction_class->operation_done = operation_done;
  transaction_class->operation_error = operation_error;
  transaction_class->choose_remote_for_ref = choose_remote_for_ref;
  transaction_class->end_of_lifed_with_rebase = end_of_lifed_with_rebase;
  transaction_class->run = flatpak_cli_transaction_run;
  transaction_class->webflow_start = webflow_start;
  transaction_class->webflow_done = webflow_done;
  transaction_class->basic_auth_start = basic_auth_start;
  transaction_class->install_authenticator = install_authenticator;
}

FlatpakTransaction *
flatpak_cli_transaction_new (FlatpakDir *dir,
                             gboolean    disable_interaction,
                             gboolean    stop_on_first_error,
                             gboolean    non_default_arch,
                             GError    **error)
{
  g_autoptr(FlatpakInstallation) installation = NULL;
  g_autoptr(FlatpakCliTransaction) self = NULL;

  installation = flatpak_installation_new_for_dir (dir, NULL, error);
  if (installation == NULL)
    return NULL;

  self = g_initable_new (FLATPAK_TYPE_CLI_TRANSACTION,
                         NULL, error,
                         "installation", installation,
                         NULL);
  if (self == NULL)
    return NULL;

  self->disable_interaction = disable_interaction;
  self->stop_on_first_error = stop_on_first_error;
  self->non_default_arch = non_default_arch;

  flatpak_transaction_set_no_interaction (FLATPAK_TRANSACTION (self), disable_interaction);
  flatpak_transaction_add_default_dependency_sources (FLATPAK_TRANSACTION (self));

  return (FlatpakTransaction *) g_steal_pointer (&self);
}

static gboolean
flatpak_cli_transaction_run (FlatpakTransaction *transaction,
                             GCancellable       *cancellable,
                             GError            **error)
{
  FlatpakCliTransaction *self = FLATPAK_CLI_TRANSACTION (transaction);
  gboolean res;

  res = FLATPAK_TRANSACTION_CLASS (flatpak_cli_transaction_parent_class)->run (transaction, cancellable, error);

  if (flatpak_fancy_output ())
    {
      flatpak_pty_clear_progress ();
      flatpak_disable_raw_mode ();
      flatpak_show_cursor ();
    }

  if (res && self->n_ops > 0)
    {
      const char *text;

      if (self->uninstalling + self->installing + self->updating > 1)
        text = _("Changes complete.");
      else if (self->uninstalling)
        text = _("Uninstall complete.");
      else if (self->installing)
        text = _("Installation complete.");
      else
        text = _("Updates complete.");

      if (flatpak_fancy_output ())
        {
          set_progress (self, text);
          redraw (self);
        }
      else
        g_print ("%s", text);

      g_print ("\n");
    }

  if (self->first_operation_error)
    {
      g_clear_error (error);

      /* We always want to return an error if there was some kind of operation error,
         as that causes the main CLI to return an error status. */

      if (self->stop_on_first_error)
        {
          /* For the install/stop_on_first_error we return the first operation error,
             as we have not yet printed it.  */

          g_propagate_error (error, g_steal_pointer (&self->first_operation_error));
          return FALSE;
        }
      else
        {
          /* For updates/!stop_on_first_error we already printed all errors so we make up
             a different one. */

          return flatpak_fail (error, _("There were one or more errors"));
        }
    }

  if (!res)
    return FALSE;

  return TRUE;
}
